﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.OleDb;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Transactions;
using VersionStar.App.Utilities;

namespace VersionStar.App
{
    public class DatabasePatcher
    {
        private const string DiscoDatabasePatchesFileNamePart = @"PatchStore.xml";
        private const string ScriptPathPart = @"Patches\";
        private Dictionary<string, string> _parameters;

        public DatabasePatcher(string serverName, string databaseName, Dictionary<string, string> parameters)
        {
            _parameters = parameters;
            Prepare(serverName, databaseName);
        }

        public string DiscoDatabasePatchesFileName { get { return Path.Combine(BaseDirectory, DiscoDatabasePatchesFileNamePart); } }

        public string BaseDirectory
        {
            get
            {
                if (_parameters.ContainsKey("patchPath"))
                {
                    string val;
                    _parameters.TryGetValue("patchPath", out val);
                    return val;
                }
                else
                {
                    string codeBase = Assembly.GetExecutingAssembly().CodeBase;
                    Uri uri = new Uri(codeBase);
                    string path = Uri.UnescapeDataString(uri.LocalPath);
                    return Path.GetDirectoryName(path);
                }
            }
        }

        public string ScriptPath { get { return Path.Combine(BaseDirectory, ScriptPathPart); } }
        public string ServerName { get; set; }
        public string DatabaseName { get; set; }
        public List<AppliedPatch> AppliedPatches { get; set; }
        public List<Patch> AvailablePatches { get; set; }
        public bool Initialized { get; set; }
        public PatchStore PatchStore { get; set; }
        public OleDbConnection Connection { get; set; }

        public string SqlConnectionString
        {
            get
            {
                return
                    PatchStore
                        .connectionStringBase
                        .Replace("%server%", ServerName)
                        .Replace("%database%", DatabaseName);
            }
        }

        public string LastAppliedVersion
        {
            get
            {
                if (!AppliedPatches.Any())
                {
                    return "-";
                }
                else
                {
                    return AppliedPatches.Last().PatchNumber;
                }
            }
        }

        public void Initialize()
        {
            if (!Initialized)
            {
                Initialized = true;
                LoadAvailablePatches();
                VerifyNotExtraPatchesArePresent();
                CreateConnection();
                CreateAppliedPatchesTableIfRequired();
                LoadAppliedPatches();
            }
        }

        public void ApplyPatches()
        {
            Logger.Log("ApplyPatches");
            Initialize();
            VerifyPatchesAreValid();

            Logger.Log("  Last applied patch was: {0}", LastAppliedVersion);

            if (GetNextPatchToApply() == null)
            {
                Logger.Log(" ** There are no new patches to apply! **");
                return;
            }

            using (OleDbTransaction transaction = Connection.BeginTransaction())
            {
                Patch patchToApply;
                while ((patchToApply = GetNextPatchToApply()) != null)
                {
                    LoadPatchScript(patchToApply);
                    ApplyAndRegisterPatch(patchToApply, transaction);
                }

                transaction.Commit();
            }
        }

        public void ApplyAndRegisterPatch(Patch patchToApply, OleDbTransaction transaction)
        {
            Logger.Log("ApplyAndRegisterPatch {0}", patchToApply.patchNumber);

            if (string.IsNullOrWhiteSpace(patchToApply.Script))
            {
                throw
                    new InvalidOperationException(
                        string.Format(
                            "No script loaded for {0} {1} (from {2})",
                            patchToApply.patchNumber,
                            patchToApply.Description,
                            patchToApply.FileName));
            }

            ApplyPatch(patchToApply, transaction);
            RegisterPatchAsApplied(patchToApply, transaction);
        }

        public void VerifyPatchesAreValid()
        {
            VerifyThatAllOldPatchesHaveBeenApplied();
        }

        public Patch GetNextPatchToApply()
        {
            if (!AvailablePatches.Any())
            {
                return null;
            }

            if (!AppliedPatches.Any())
            {
                return AvailablePatches.First();
            }

            string lastAppliedPatchNumber = AppliedPatches.Last().PatchNumber;
            Patch comingPatch = AvailablePatches.FirstOrDefault(patch => patch.patchNumber.CompareTo(lastAppliedPatchNumber) > 0);
            return comingPatch;
        }

        public void CreateConnection()
        {
            if (Connection != null)
            {
                return;
            }

            Logger.Log("Creating data connection for {0}/{1}", ServerName, DatabaseName);
            Connection = new OleDbConnection(SqlConnectionString);

            Logger.Log("  * Open data connection for {0}/{1}", ServerName, DatabaseName);
            Connection.Open();
        }

        public void LoadAvailablePatches()
        {
            Logger.Log("Loading available patches from {0}", DiscoDatabasePatchesFileName);
            if (!File.Exists(DiscoDatabasePatchesFileName))
            {
                throw new InvalidOperationException(
                    "Unable to find PatchStore at DiscoDatabasePatchesFileName. \nYou can use patchPath=xxx in the command line to specify a different path.");
            }
            PatchStore = PatchStore.LoadFromFile(DiscoDatabasePatchesFileName);
            AvailablePatches = PatchStore.Patches.OrderBy(x => x.patchNumber).ToList();
            AvailablePatches.ForEach(patch => patch.SetFileNameAndLoadScript(ScriptPath));
            Logger.Log("  Found {0} available patches", AvailablePatches.Count);
            CheckForDuplictes();
        }

        private void VerifyNotExtraPatchesArePresent()
        {
            string filter = ScriptPath + @"\*.sql";
            Logger.Log("Checking for extraneous patches matching " + filter);
            List<string> patches = Directory.EnumerateFiles(ScriptPath, "*.sql").ToList();

            foreach (string patchFileName in patches)
            {
                if (AvailablePatches.All(patch => patch.FileName != patchFileName))
                {
                    throw new InvalidOperationException(
                        string.Format(
                            "There are extraneous in the patch directory: \n{0}",
                            patchFileName));
                }
            }
        }

        private void CreateAppliedPatchesTableIfRequired()
        {
            Logger.Log("Create applied patches table if missing");
            SqlScriptExecutor executor = new SqlScriptExecutor(Connection, null);
            executor.Execute(PatchStore.AppliedPatches.CreateAppliedPatchTable);
        }

        private void CheckForDuplictes()
        {
            foreach (Patch patch in AvailablePatches)
            {
                if (AvailablePatches.Count(p => p.patchNumber == patch.patchNumber) > 1)
                {
                    throw new InvalidOperationException(
                        string.Format(
                            "There are duplicates for patch {0}!",
                            patch.patchNumber));
                }
            }
        }

        public void LoadAppliedPatches()
        {
            Logger.Log("Loading applied patches {0}/{1} (using {2})", ServerName, DatabaseName, PatchStore.AppliedPatches.SelectAppliedPatches);

            OleDbCommand cmd = new OleDbCommand
                {
                    CommandText = PatchStore.AppliedPatches.SelectAppliedPatches,
                    CommandType = CommandType.Text,
                    Connection = Connection
                };

            OleDbDataReader reader = cmd.ExecuteReader();
            if (reader.HasRows)
            {
                while (reader.Read())
                {
                    AppliedPatch appliedPatch = new AppliedPatch
                        {
                            PatchNumber = reader.GetString(0),
                            Description = reader.GetString(1)
                        };
                    AppliedPatches.Add(appliedPatch);
                }
            }

            AppliedPatches = AppliedPatches.OrderBy(x => x.PatchNumber).ToList();
            Logger.Log("  Found {0} applied patches", AppliedPatches.Count);
        }

        private void VerifyThatAllOldPatchesHaveBeenApplied()
        {
            if (!AppliedPatches.Any())
            {
                return;
            }

            string lastAppliedVersion = AppliedPatches.Last().PatchNumber;
            List<Patch> oldPatches = AvailablePatches.Where(patch => patch.patchNumber.CompareTo(lastAppliedVersion) <= 0).ToList();
            foreach (Patch oldPatch in oldPatches)
            {
                if (AppliedPatches.Any(ap => ap.PatchNumber == oldPatch.patchNumber) == false)
                {
                    throw new InvalidOperationException(
                        string.Format(
                            "Patch {0} should have been applied, but it hasn't (latest applied patch={1})!",
                            oldPatch.patchNumber,
                            lastAppliedVersion));
                }
            }
        }

        private void LoadPatchScript(Patch patchToApply)
        {
            Logger.Log("  LoadPatchDetails {0}", patchToApply.patchNumber);
            SetPatchDescription(patchToApply, patchToApply.FileName);
        }

        private static void SetPatchDescription(Patch patchToApply, string fileName)
        {
            patchToApply.Description = StringTools.GetBetween(fileName, patchToApply.patchNumber, ".sql").Trim();
            if (patchToApply.Description.StartsWith("-"))
            {
                patchToApply.Description = patchToApply.Description.Substring(1).Trim();
            }
        }

        private void ApplyPatch(Patch patchToApply, OleDbTransaction transaction)
        {
            Logger.Log("  ApplyPatch {0}", patchToApply.patchNumber);
            SqlScriptExecutor executor = new SqlScriptExecutor(Connection, transaction);
            //Logger.Log("  applying patch: " + patchToApply.Script);
            executor.Execute(patchToApply.Script);
        }

        private void RegisterPatchAsApplied(Patch patchToApply, OleDbTransaction transaction)
        {
            Logger.Log("  RegisterPatchAsApplied {0}", patchToApply.patchNumber);

            string sql =
                PatchStore.AppliedPatches.AddAppliedPatch
                          .Replace("%patchNumber%", patchToApply.patchNumber)
                          .Replace("%description%", patchToApply.Description);

            SqlScriptExecutor executor = new SqlScriptExecutor(Connection, transaction);
            executor.Execute(sql);

            AppliedPatch appliedPatch =
                new AppliedPatch
                    {
                        PatchNumber = patchToApply.patchNumber,
                        Description = patchToApply.Description
                    };
            AppliedPatches.Add(appliedPatch);
        }

        private void Prepare(string serverName, string databaseName)
        {
            Logger.Log("Creating DatabasePatcher for {0} / {1}...", serverName, databaseName);
            Logger.Log("  UserName = {0}", Environment.UserDomainName + "\\" + Environment.UserName);
            Logger.Log("  DiscoDatabasePatchesFileName = {0}", DiscoDatabasePatchesFileName);
            Logger.Log("  ScriptDirectory = {0}", ScriptPath);
            Logger.Log(string.Empty);
            ServerName = serverName;
            DatabaseName = databaseName;
            AppliedPatches = new List<AppliedPatch>();
            AvailablePatches = new List<Patch>();
        }
    }
}